<?php

namespace mongrove;

use \ArrayAccess;
use \Exception;

/**
 * The Record class is the basic means through which Mongrove can be used.
 * The class is both the specification of types and documents which can be
 * stored and is also capable of providing simple means to query the
 * collection of the defined Record type as well.
 *
 * The internal structure of a Record is to be defined through overriding the
 * static getStructure method.
 *
 * Additionally the behavior of the naming of the collection and/or database
 * in which the Record is to be stored can be changed by overriding the
 * static getCollectionName and getDatabaseName methods.
 *
 * @author Gido Hakvoort <gido@hakvoort.it>
 * @author Michiel Hakvoort <michiel@hakvoort.it>
 *
 */
abstract class Record implements ArrayAccess {

    protected $_isDeleted = false;

    /**
     *
     * @var \mongrove\Structure
     */
    protected $_structure = null;
    protected $_id = null;

    public function __construct() {
        $this->_structure = self :: getStructure();

        if($this->_structure === null) {
            throw new Exception("Structure not set.");
        }
    }

    /**
     * Get the Structure which defines the internal structure of
     * this Record type.
     *
     * @return \mongrove\Structure
     */
    public static function getStructure() {
        $class = get_called_class();

        if($class !==  __CLASS__) {
            return $class :: getStructure();
        } else {
            throw new Exception('getStructure() must be called on an implementation of Record.');
        }
    }

    /**
     * Get the Collection object which is responsible for manipulating
     * and querying Records of this Record type.
     *
     * @return \mongrove\Collection
     */
    public static function getCollection() {
        $class = strtolower(get_called_class());

        return CollectionManager :: getCollectionManager()->getCollection($class, $class :: getCollectionName(), $class :: getDatabaseName());
    }

    /**
     * Get a derived hierarchy of types for this Record.
     *
     * @return array
     */
    public static function getTypeHierarchy() {
        $type = get_called_class();

        $types = array();

        while($type !== __CLASS__) {
            $types[] = strtolower($type);
            $type = get_parent_class($type);
        }

        return $types;
    }

    /**
     * Get the name of the Mongo collection in which this Record
     * should reside. By default the collection name will equal the
     * fully qualified class name of the Record. This will result
     * in subclasses to be stored in different collections. If
     * queries should be performed over Records of different
     * types but with the same parent type, these Records should
     * be stored in the same collection.
     *
     * @return string The name of the collection in which the Record should be stored
     */
    public static function getCollectionName() {
        return strtolower(get_called_class());
    }

    /**
     * Get the name of the Mongo database in which this Record should reside.
     *
     * If the returned name is null, default behavior results in the selection
     * of the first Mongo handle containing a Mongo collection which equals this
     * Record's getCollectionName(). If no Mongo database contains a collection
     * of said name, the first Mongo handle will be selected.
     *
     * @return string|null
     */
    public static function getDatabaseName() {
        return null;
    }

    /**
     *
     * Get a Field value from the Record
     *
     * @param string $name
     *
     * @return mixed
     */
    public function __get($name) {
        return $this->_structure->__get($name);
    }

    /**
     * Set the value of a Field contained in this Record.
     *
     * @param string $name
     * @param mixed $value
     *
     * @return mixed The set value
     */
    public function __set($name, $value) {
        if($name === Constant :: STRUCTURE || $name === Constant :: ID) {
            throw new Exception("The field name '{$name}' can not be used");
        }

        $this->_structure->__set($name, $value);

        return $value;
    }

    /**
     * Get the id of the Record
     *
     * @return MongoId
     */
    public function getId() {
        return $this->_id;
    }

    /**
     * Set the id of the Record
     *
     * @param MongoId $id
     */
    public function setId($id) {
        $this->_id = $id;
    }

    /**
     * Check whether the record is deleted.
     *
     * @return boolean True if the Record is marked as deleted
     */
    public function isDeleted() {
        return $this->_isDeleted;
    }

    /**
     * Get a dehydrated, Mongo compatible, representation
     * of the Record.
     *
     *  @return array The dehydrated representation of the Record
     */
    public function dehydrate() {
        return $this->_structure->dehydrate();
    }

    /**
     * Get all mutations which need to be applied in order to
     * persist this Record successfully.
     *
     * @return array
     */
    public function getMutations() {
        return $this->_structure->getMutations();
    }

    /**
     * Hydrate the Record with a Mongo array
     *
     * @param array $value The value to hydrate from
     *
     * @return \mongrove\Record
     */
    public function hydrate($value) {
        if(isset($value[Constant :: ID])) {
            $this->setId((string)$value[Constant :: ID]);
        }

        $this->_structure->hydrate($value);

        return $this;
    }

    /**
     * Set the Record's and all underlying Fields' state to clean.
     * A cleaned Record will have no pending mutations which need
     * to be executed in order for the Record's content to be
     * persisted.
     */
    public function clean() {
        $this->_structure->clean();
    }

    /**
     * Save the Record in its Collection.
     *
     * @return boolean True if the Record was successfully persisted or unchanged prior to saving
     */
    public function save() {
        $isNew = $this->_id === null;

        $success = false;

        if($isNew ? $this->beforeInsert() : $this->beforeUpdate()) {
            $success = self :: getCollection()->save($this);
        }

        $isNew ? $this->afterInsert($success) : $this->afterUpdate($success);

        return $success;
    }

    /**
     * Delete the record from its containing Collection.
     *
     * @return boolean True if the Record was successfully deleted
     */
    public function delete() {
        $success = false;

        if($this->beforeDelete() && self :: getCollection()->delete($this)) {
            $this->_isDeleted = true;
            $this->_id = null;

            $success = true;
        }

        $this->afterDelete($success);

        return $success;
    }

    /**
     * Triggered and queried before the insert operation is performed.
     * If execution of this method returns false, the insert operation
     * is cancelled.
     *
     * @return boolean False if the insert operation is to be cancelled
     */
    protected function beforeInsert() {

        return true;
    }

    /**
     * Triggered after an insert operation was called on this Record.
     *
     * @param boolean $wasSuccessful True if the insert operation was successful
     */
    protected function afterInsert($wasSuccessful) {

    }

    /**
     * Triggered and queried before the update operation is performed.
     * If execution of this method returns false, the update operation
     * is cancelled.
     *
     * @return boolean False if the update operation is to be cancelled
     */
    protected function beforeUpdate() {

        return true;
    }

    /**
     * Triggered after an update operation was called on this Record.
     *
     * @param boolean $wasSuccessful True if the update operation was successful
     */
    protected function afterUpdate($wasSuccessful) {

    }

    /**
     * Triggered and queried before the delete operation is performed.
     * If execution of this method returns false, the delete operation
     * is cancelled.
     *
     * @return boolean False if the insert operation is to be cancelled
     */
    protected function beforeDelete() {

        return true;
    }

    /**
     * Triggered after a delete operation was called on this Record.
     *
     * @param boolean $wasSuccessful True if the delete operation was successful
     */
    protected function afterDelete($wasSuccessful) {

    }

    /*
     * ArrayAccess
     */

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetExists()
     */
    public function offsetExists($offset) {
        return $this->_structure->offsetExists($offset);
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetGet()
     */
    public function offsetGet($offset) {
        return $this->_structure->offsetGet($offset);
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetSet()
     */
    public function offsetSet($offset, $value) {
        $this->_structure->offsetSet($offset, $value);
    }

    /**
     * (non-PHPdoc)
     * @see ArrayAccess::offsetUnset()
     */
    public function offsetUnset($offset) {
        $this->_structure->offsetUnset($offset);
    }

    /**
     * Get the type of the Record. This type is a string
     * representation of the Record name and is used for
     * finding Records by type from a Collection.
     *
     * @return string The type of the Record
     */
    public static final function getType() {
        return mb_strtolower(get_called_class());
    }

    /*
     * Some utility methods for fast querying
     */

    /**
     * The __callStatic method is a proxy towards this Record's type Collection
     * __call method. This is intended for the usage of simple findBy* and
     * findOneBy* methods.
     *
     * @param $name
     * @param $arguments
     *
     * @throws \Exception In case an invalid method is executed
     *
     * @return Record|QueryResult
     */
    public static function __callStatic($name, $arguments) {
        return self :: getCollection()->__call($name, $arguments);
    }

    /**
     * Create a query for searching through this Record type.
     *
     * @return \mongrove\Query
     */
    public static function createQuery() {
        return self :: getCollection()->createQuery();
    }

    /**
     * Return all instances of this Record type
     *
     * @return array of instances of this Record type
     */
    public static function findAll() {
        return self :: getCollection()->findAll();
    }

    /**
     * Find all instances of this Record type by the given field.
     *
     * @param string $field The field to match
     * @param mixed $value The value the field should match
     *
     * @return \mongrove\QueryResult
     */
    public static function findBy($field, $value) {
        return self :: getCollection()->findBy($field, $value);
    }

    /**
     * Find a single instance of this Record type by the given field.
     *
     * @param string $field The field to match
     * @param mixed $value The value the field should match
     *
     * @return \mongrove\Record
     */
    public static function findOneBy($field, $value) {
        return self :: getCollection()->findOneBy($field, $value);
    }

    /**
     * Find a Record by its Mongo id
     *
     * @param string $value
     *
     * @return \mongrove\Record
     */
    public static function findOneById($value) {
        return self :: getCollection()->findOneById($value);
    }
}